API Reference
Complete reference for Loopar's server-side APIs.
Core Objects
| Object | Description |
|---|---|
loopar | Global framework object |
BaseDocument | Base class for Models |
BaseController | Base class for Controllers |
Import Patterns
// In Model (.js)
import { BaseDocument } from "loopar";
import loopar from "loopar";
// In Controller (-controller.js)
import { BaseController } from "loopar";
import loopar from "loopar";
// In any server file
import loopar from "loopar";
Quick Reference
Document Operations
const doc = await loopar.getDocument("Entity", "name");
const doc = await loopar.newDocument("Entity");
await doc.save();
await doc.delete();
Database Queries
const all = await loopar.db.getAll("Entity", options);
const count = await loopar.db.count("Entity", filters);
const value = await loopar.db.getValue("Entity", "name", "field");
Utilities
const user = loopar.session.user;
const config = loopar.config.get("key");
await loopar.sendEmail({ to, subject, body });
loopar (Global Object)
The main framework object available throughout the server-side code.
import loopar from "loopar";
Document Methods
loopar.getDocument(entity, name)
Retrieve an existing document by name.
const customer = await loopar.getDocument("Customer", "CUST-0001");
// Access fields
console.log(customer.email);
console.log(customer.status);
// Call model methods
const fullName = customer.getFullName();
| Parameter | Type | Description |
|---|---|---|
entity | string | Entity name |
name | string | Document name/ID |
Returns: Document instance
Throws: Error if document not found
loopar.newDocument(entity, data?)
Create a new document instance.
// Create empty
const customer = await loopar.newDocument("Customer");
customer.customer_name = "John Doe";
customer.email = "john@example.com";
await customer.save();
// Create with initial data
const task = await loopar.newDocument("Task", {
title: "New Task",
status: "Open",
priority: "High"
});
await task.save();
| Parameter | Type | Description |
|---|---|---|
entity | string | Entity name |
data | object | Initial field values (optional) |
Returns: New document instance (unsaved)
loopar.deleteDocument(entity, name)
Delete a document by name.
await loopar.deleteDocument("Customer", "CUST-0001");
| Parameter | Type | Description |
|---|---|---|
entity | string | Entity name |
name | string | Document name/ID |
Session & User
loopar.session
Current session information.
// Current user
const user = loopar.session.user;
const userName = loopar.session.user_name;
// Check if logged in
if (loopar.session.user) {
// User is authenticated
}
// Current site/tenant
const site = loopar.session.site;
| Property | Type | Description |
|---|---|---|
user | string | Current user ID |
user_name | string | User display name |
site | string | Current tenant name |
is_admin | boolean | Is administrator |
Configuration
loopar.config
Access framework configuration.
// Get config value
const dbType = loopar.config.get("db_type");
const port = loopar.config.get("port");
// Get with default
const timeout = loopar.config.get("timeout", 5000);
Utilities
loopar.sendEmail(options)
Send an email.
await loopar.sendEmail({
to: "customer@example.com",
subject: "Welcome!",
body: "<h1>Welcome to our platform</h1>",
// Optional
from: "noreply@yoursite.com",
cc: ["manager@yoursite.com"],
attachments: [{ filename: "doc.pdf", path: "/path/to/doc.pdf" }]
});
| Option | Type | Description |
|---|---|---|
to | string/array | Recipient(s) |
subject | string | Email subject |
body | string | HTML body |
from | string | Sender (optional) |
cc | array | CC recipients |
bcc | array | BCC recipients |
attachments | array | File attachments |
loopar.throw(message, httpCode?)
Throw an error with optional HTTP status code.
if (!customer) {
loopar.throw("Customer not found", 404);
}
if (!hasPermission) {
loopar.throw("Access denied", 403);
}
loopar.log(message, level?)
Log a message.
loopar.log("Processing started");
loopar.log("Warning: slow query", "warn");
loopar.log("Error occurred", "error");
| Level | Description |
|---|---|
info | Information (default) |
warn | Warning |
error | Error |
debug | Debug (dev only) |
loopar.db (Database API)
Direct database operations for querying and manipulating data.
import loopar from "loopar";
const results = await loopar.db.getAll("Customer");
Query Methods
loopar.db.getAll(entity, options?)
Get multiple documents with filtering, sorting, and pagination.
// Simple: get all
const customers = await loopar.db.getAll("Customer");
// With filters
const activeCustomers = await loopar.db.getAll("Customer", {
filters: { status: "Active" }
});
// Complex query
const results = await loopar.db.getAll("Task", {
filters: {
status: ["Open", "In Progress"], // OR condition
priority: "High",
project: "PROJECT-001"
},
fields: ["name", "title", "status", "due_date"],
orderBy: "due_date ASC",
limit: 20,
offset: 0
});
| Option | Type | Description |
|---|---|---|
filters | object | Field conditions |
fields | array | Fields to return |
orderBy | string | Sort order (field ASC/DESC) |
limit | number | Max records to return |
offset | number | Records to skip |
Filter Operators:
// Exact match
{ status: "Active" }
// OR condition (array)
{ status: ["Open", "In Progress"] }
// Multiple conditions (AND)
{ status: "Active", priority: "High" }
// With operators
{ amount: [">", 1000] }
{ created_at: [">=", "2024-01-01"] }
{ name: ["like", "%john%"] }
loopar.db.getOne(entity, filters)
Get a single document matching filters.
const customer = await loopar.db.getOne("Customer", {
email: "john@example.com"
});
if (customer) {
console.log(customer.name);
}
Returns: Single document object or null
loopar.db.getValue(entity, name, field)
Get a single field value from a document.
const email = await loopar.db.getValue("Customer", "CUST-0001", "email");
const status = await loopar.db.getValue("Task", "TASK-0001", "status");
| Parameter | Type | Description |
|---|---|---|
entity | string | Entity name |
name | string | Document name |
field | string | Field to retrieve |
Returns: Field value or null
loopar.db.count(entity, filters?)
Count documents matching filters.
// Count all
const totalCustomers = await loopar.db.count("Customer");
// Count with filter
const activeCount = await loopar.db.count("Customer", {
status: "Active"
});
// Count with multiple conditions
const urgentOpen = await loopar.db.count("Task", {
status: "Open",
priority: "Urgent"
});
Returns: Number
loopar.db.exists(entity, name)
Check if a document exists.
const exists = await loopar.db.exists("Customer", "CUST-0001");
if (!exists) {
// Create new customer
}
Returns: Boolean
Modify Methods
loopar.db.setValue(entity, name, field, value)
Update a single field value.
await loopar.db.setValue("Customer", "CUST-0001", "status", "VIP");
await loopar.db.setValue("Task", "TASK-0001", "completed_at", new Date());
Note: This bypasses model hooks. Use
doc.save()for full lifecycle.
loopar.db.setValues(entity, name, values)
Update multiple field values.
await loopar.db.setValues("Customer", "CUST-0001", {
status: "VIP",
credit_limit: 50000,
modified_at: new Date()
});
Raw SQL
loopar.db.execute(sql, params?)
Execute raw SQL query.
// Simple query
const results = await loopar.db.execute(
"SELECT * FROM Customer WHERE status = ?",
["Active"]
);
// Complex aggregation
const stats = await loopar.db.execute(`
SELECT
project,
COUNT(*) as task_count,
SUM(CASE WHEN status = 'Completed' THEN 1 ELSE 0 END) as completed
FROM Task
WHERE created_at > ?
GROUP BY project
`, ["2024-01-01"]);
Warning: Use parameterized queries to prevent SQL injection.
Transactions
loopar.db.transaction(callback)
Execute operations in a transaction.
await loopar.db.transaction(async (trx) => {
// All operations use same transaction
const order = await loopar.newDocument("Order");
order.customer = "CUST-0001";
order.total = 1500;
await order.save({ trx });
// Update customer balance
const customer = await loopar.getDocument("Customer", "CUST-0001");
customer.balance += 1500;
await customer.save({ trx });
// If any operation fails, all are rolled back
});
BaseDocument
Base class for all Models. Extend this to add custom logic to your entities.
// customer.js (MODEL)
import { BaseDocument } from "loopar";
export default class Customer extends BaseDocument {
constructor(props) {
super(props);
}
}
Instance Properties
Field Values
Access and set field values directly.
// Read fields
const name = this.customer_name;
const email = this.email;
const status = this.status;
// Set fields
this.status = "Active";
this.modified_at = new Date();
this.total = this.subtotal + this.tax;
this.name
The document's unique identifier.
console.log(this.name); // "CUST-0001"
this.isNew
Whether this is a new (unsaved) document.
async beforeSave() {
if (this.isNew) {
this.created_at = new Date();
this.created_by = this.session.user;
}
}
this.session
Current session information.
const currentUser = this.session.user;
const currentSite = this.session.site;
this.request
HTTP request object (when triggered via API).
const body = this.request?.body;
const query = this.request?.query;
const headers = this.request?.headers;
Instance Methods
this.save(options?)
Save the document to database.
// Simple save
await this.save();
// Save with options
await this.save({
ignore_permissions: true, // Skip permission check
ignore_validate: false, // Skip validation
trx: transaction // Use transaction
});
Triggers: validate() → beforeSave() → beforeInsert()/beforeUpdate() → DB → afterInsert()/afterUpdate() → afterSave()
this.delete()
Delete the document.
await this.delete();
Triggers: beforeDelete() → DB → afterDelete()
this.reload()
Reload document from database.
await this.reload();
this.getData()
Get document as plain object.
const data = this.getData();
// { name: "CUST-0001", customer_name: "John", email: "john@...", ... }
Lifecycle Hooks
Override these methods to add custom logic.
async validate()
Validate before any save operation.
async validate() {
if (!this.email) {
throw new Error("Email is required");
}
if (this.amount < 0) {
throw new Error("Amount cannot be negative");
}
// Check for duplicates
const existing = await loopar.db.getOne("Customer", {
email: this.email,
name: ["!=", this.name] // Exclude self
});
if (existing) {
throw new Error("Email already exists");
}
}
async beforeInsert()
Before saving a NEW document.
async beforeInsert() {
this.created_at = new Date();
this.created_by = this.session.user;
this.status = this.status || "Draft";
// Generate custom ID
this.customer_id = await this.generateCustomerId();
}
async afterInsert()
After saving a NEW document.
async afterInsert() {
// Send welcome email
await loopar.sendEmail({
to: this.email,
subject: "Welcome!",
body: `Hello ${this.customer_name}!`
});
// Create related records
const settings = await loopar.newDocument("Customer Settings");
settings.customer = this.name;
await settings.save();
}
async beforeUpdate()
Before updating an EXISTING document.
async beforeUpdate() {
this.modified_at = new Date();
this.modified_by = this.session.user;
}
async afterUpdate()
After updating an EXISTING document.
async afterUpdate() {
// Log changes
await this.logChanges();
// Notify if status changed
if (this.status !== this._previousStatus) {
await this.notifyStatusChange();
}
}
async beforeSave()
Before any save (insert OR update).
async beforeSave() {
// Calculate totals
this.total = this.subtotal + this.tax - this.discount;
// Update full name
this.full_name = `${this.first_name} ${this.last_name}`;
}
async afterSave()
After any save (insert OR update).
async afterSave() {
// Update related records
await this.updateRelatedRecords();
// Clear cache
await this.clearCache();
}
async beforeDelete()
Before deleting.
async beforeDelete() {
// Prevent deletion of locked records
if (this.is_locked) {
throw new Error("Cannot delete locked record");
}
// Check for dependencies
const orders = await loopar.db.count("Order", { customer: this.name });
if (orders > 0) {
throw new Error("Cannot delete customer with orders");
}
}
async afterDelete()
After deleting.
async afterDelete() {
// Clean up related data
await loopar.db.execute(
"DELETE FROM CustomerSettings WHERE customer = ?",
[this.name]
);
// Log deletion
console.log(`Customer ${this.name} deleted`);
}
BaseController
Base class for Controllers. Only methods prefixed with action are accessible via URL.
// customer-controller.js (CONTROLLER)
import { BaseController } from "loopar";
export default class CustomerController extends BaseController {
// GET/POST /api/Customer/stats
async actionStats() {
return { total: 100 };
}
}
URL Routing
| Method Name | URL | HTTP Methods |
|---|---|---|
actionView | /api/Entity/view | GET, POST |
actionCreate | /api/Entity/create | POST |
actionStats | /api/Entity/stats | GET, POST |
actionSendEmail | /api/Entity/send-email | GET, POST |
privateMethod | — | NOT accessible |
Naming: actionMyAction → /api/Entity/my-action (camelCase to kebab-case)
Instance Properties
this.data
Request data (body + query params).
async actionCreate() {
const { name, email, status } = this.data;
const customer = await loopar.newDocument("Customer", {
customer_name: name,
email,
status
});
await customer.save();
return { success: true, name: customer.name };
}
this.request
Full HTTP request object.
async actionUpload() {
const file = this.request.files?.document;
const contentType = this.request.headers["content-type"];
const method = this.request.method;
// ...
}
| Property | Description |
|---|---|
method | HTTP method (GET, POST, etc.) |
headers | Request headers |
query | Query string params |
body | Request body |
files | Uploaded files |
params | URL params |
this.response
HTTP response object.
async actionDownload() {
this.response.setHeader("Content-Type", "application/pdf");
this.response.setHeader("Content-Disposition", "attachment; filename=report.pdf");
// Return file buffer
return fileBuffer;
}
this.session
Current session.
async actionProfile() {
const user = this.session.user;
if (!user) {
loopar.throw("Not authenticated", 401);
}
return await loopar.getDocument("User", user);
}
Action Examples
Basic CRUD Action
// POST /api/Customer/create
async actionCreate() {
const customer = await loopar.newDocument("Customer", this.data);
await customer.save();
return {
success: true,
message: "Customer created",
name: customer.name
};
}
Query Action
// GET /api/Customer/search?q=john&status=Active
async actionSearch() {
const { q, status, limit = 20 } = this.data;
const filters = {};
if (status) filters.status = status;
if (q) filters.customer_name = ["like", `%${q}%`];
const customers = await loopar.db.getAll("Customer", {
filters,
limit: parseInt(limit),
orderBy: "customer_name ASC"
});
return { customers };
}
Action with Document
// POST /api/Customer/upgrade-to-vip
async actionUpgradeToVip() {
const { name } = this.data;
if (!name) {
loopar.throw("Customer name required", 400);
}
const customer = await loopar.getDocument("Customer", name);
customer.status = "VIP";
customer.vip_since = new Date();
await customer.save();
// Call model method
const discount = customer.calculateVIPDiscount();
return {
success: true,
message: `${customer.customer_name} is now VIP`,
discount
};
}
Stats/Aggregation Action
// GET /api/Task/dashboard
async actionDashboard() {
const user = this.session.user;
const [open, inProgress, completed, overdue] = await Promise.all([
loopar.db.count("Task", { status: "Open", assigned_to: user }),
loopar.db.count("Task", { status: "In Progress", assigned_to: user }),
loopar.db.count("Task", { status: "Completed", assigned_to: user }),
loopar.db.execute(
`SELECT COUNT(*) as count FROM Task
WHERE assigned_to = ? AND due_date < NOW() AND status != 'Completed'`,
[user]
).then(r => r[0]?.count || 0)
]);
return {
open,
inProgress,
completed,
overdue,
total: open + inProgress + completed
};
}
File Download Action
// GET /api/Report/export?format=csv
async actionExport() {
const { format = "csv" } = this.data;
const data = await loopar.db.getAll("Customer");
if (format === "csv") {
const csv = this.convertToCSV(data);
this.response.setHeader("Content-Type", "text/csv");
this.response.setHeader("Content-Disposition", "attachment; filename=customers.csv");
return csv;
}
return { data };
}
// Private helper (NOT URL accessible)
convertToCSV(data) {
// ...
}
Error Handling
async actionRiskyOperation() {
try {
// Risky operation
await this.performOperation();
return { success: true };
} catch (error) {
// Log error
loopar.log(error.message, "error");
// Return error response
loopar.throw(error.message, 500);
}
}
Authentication Check
async actionSecureAction() {
// Check authentication
if (!this.session.user) {
loopar.throw("Authentication required", 401);
}
// Check admin
if (!this.session.is_admin) {
loopar.throw("Admin access required", 403);
}
// Proceed with action
return { secret: "data" };
}
REST API
Loopar automatically generates REST endpoints for all entities.
Auto-Generated Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/{Entity} | List documents |
GET | /api/{Entity}/{name} | Get single document |
POST | /api/{Entity} | Create document |
PUT | /api/{Entity}/{name} | Update document |
DELETE | /api/{Entity}/{name} | Delete document |
List Documents
GET /api/Customer
Query Parameters:
| Param | Description | Example |
|---|---|---|
page | Page number | ?page=2 |
limit | Records per page | ?limit=50 |
search | Search term | ?search=john |
order_by | Sort field | ?order_by=created_at |
order | Sort direction | ?order=desc |
fields | Fields to return | ?fields=name,email |
{field} | Filter by field | ?status=Active |
Example:
GET /api/Customer?status=Active&limit=20&order_by=customer_name&order=asc
Response:
{
"data": [
{ "name": "CUST-0001", "customer_name": "John", "email": "john@..." },
{ "name": "CUST-0002", "customer_name": "Jane", "email": "jane@..." }
],
"total": 150,
"page": 1,
"limit": 20,
"pages": 8
}
Get Single Document
GET /api/Customer/CUST-0001
Response:
{
"name": "CUST-0001",
"customer_name": "John Doe",
"email": "john@example.com",
"status": "Active",
"created_at": "2024-01-15T10:30:00Z"
}
Create Document
POST /api/Customer
Content-Type: application/json
{
"customer_name": "New Customer",
"email": "new@example.com",
"status": "Lead"
}
Response:
{
"success": true,
"name": "CUST-0003",
"message": "Document created"
}
Update Document
PUT /api/Customer/CUST-0001
Content-Type: application/json
{
"status": "VIP",
"credit_limit": 50000
}
Response:
{
"success": true,
"name": "CUST-0001",
"message": "Document updated"
}
Delete Document
DELETE /api/Customer/CUST-0001
Response:
{
"success": true,
"message": "Document deleted"
}
Custom Actions
Call controller actions:
# GET action
GET /api/Customer/stats
# POST action with data
POST /api/Customer/upgrade-to-vip
Content-Type: application/json
{
"name": "CUST-0001"
}
Error Responses
{
"error": true,
"message": "Customer not found",
"status": 404
}
| Status | Meaning |
|---|---|
400 | Bad request / Validation error |
401 | Authentication required |
403 | Permission denied |
404 | Document not found |
500 | Server error |
Authentication
Include session token in requests:
GET /api/Customer
Authorization: Bearer {token}
Or use cookie-based session after login.
Client-Side API
React hooks and utilities for client-side code (.jsx files).
useDocument Hook
Access and manipulate the current document in a form.
import { useDocument } from "@loopar/components";
export default function CustomerForm(props) {
const {
document, // Current document data
setValue, // Set field value
getValue, // Get field value
save, // Save document
loading, // Loading state
errors // Validation errors
} = useDocument();
return (
<div>
<h1>{document.customer_name}</h1>
<p>Status: {document.status}</p>
<button
onClick={() => setValue("status", "VIP")}
disabled={loading}
>
Upgrade to VIP
</button>
<button onClick={save} disabled={loading}>
{loading ? "Saving..." : "Save"}
</button>
</div>
);
}
setValue(field, value)
Update a field value.
setValue("status", "Active");
setValue("amount", 1500.50);
setValue("items", [...document.items, newItem]);
getValue(field)
Get current field value.
const status = getValue("status");
const total = getValue("total");
save()
Save the document.
const handleSave = async () => {
try {
await save();
toast.success("Saved successfully!");
} catch (error) {
toast.error(error.message);
}
};
useLoopar Hook
Access Loopar utilities.
import { useLoopar } from "@loopar/components";
export default function MyComponent() {
const loopar = useLoopar();
const handleAction = async () => {
const result = await loopar.call({
entity: "Customer",
action: "stats"
});
console.log(result);
};
return <button onClick={handleAction}>Get Stats</button>;
}
loopar.call(options)
Call a server action.
// GET action
const stats = await loopar.call({
entity: "Customer",
action: "stats"
});
// POST action with data
const result = await loopar.call({
entity: "Customer",
action: "upgrade-to-vip",
method: "POST",
data: { name: "CUST-0001" }
});
loopar.navigate(path)
Navigate to a different page.
loopar.navigate("/desk/Customer/list");
loopar.navigate("/desk/Customer/edit/CUST-0001");
BaseForm Component
Wrapper for entity forms.
import { BaseForm } from "@loopar/components";
export default function CustomerForm(props) {
return (
<BaseForm {...props}>
{/* Custom content renders above default fields */}
<div className="custom-header">
{/* Custom UI */}
</div>
{/* Default fields render automatically */}
</BaseForm>
);
}
Utility Components
import {
Button,
Input,
Select,
Badge,
Alert,
Card,
Table
} from "@loopar/components";
// Or from shadcn/ui
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
Example: Custom Form UI
import { BaseForm, useDocument } from "@loopar/components";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { CheckCircle, AlertTriangle } from "lucide-react";
export default function TaskForm(props) {
const { document, setValue, save, loading } = useDocument();
const markComplete = async () => {
setValue("status", "Completed");
setValue("completed_at", new Date().toISOString());
await save();
};
const isOverdue = document.due_date &&
new Date(document.due_date) < new Date() &&
document.status !== "Completed";
return (
<BaseForm {...props}>
{/* Status Header */}
<div className="flex items-center justify-between p-4 bg-muted rounded-lg mb-6">
<div className="flex items-center gap-2">
<h2 className="font-semibold">{document.title || "New Task"}</h2>
<Badge variant={document.status === "Completed" ? "success" : "default"}>
{document.status}
</Badge>
</div>
{document.status !== "Completed" && (
<Button onClick={markComplete} disabled={loading}>
<CheckCircle className="w-4 h-4 mr-2" />
Mark Complete
</Button>
)}
</div>
{/* Overdue Warning */}
{isOverdue && (
<div className="p-3 mb-4 bg-destructive/10 text-destructive rounded-lg flex items-center gap-2">
<AlertTriangle className="w-4 h-4" />
This task is overdue!
</div>
)}
</BaseForm>
);
}